"""
Module to support Bank Operations.

Bank operations are atomic, you can run many threads, each one with their own
Bank instances and operations will be performed in a transactional way.

Last Modified: $Id: bank.py 28 2005-05-26 00:01:44Z gustavo $

@author: U{Gustavo Sverzut Barbieri <barbieri@gmail.com>}
@license: GNU GPL
@copyright: (C) 2005 Gustavo Sverzut Barbieri
@version: $Rev: 28 $
"""

import db









class Bank( object ):
    """
    Bank API: do bank operations.


    Create a Bank instance:

    >>> bank = Bank()
    >>> bank.__db_clear__() # Force DB being Empty. Since DB is shared using
    ...                     # singletons, it can contain data from previous
    ...                     # tests.


    Do operations. accounts that doesn't exists are considered to have 0 money.
    Accounts are created as need, just deposit, withdraw or transfer values to
    it:

    >>> bank.deposit( 1, 100 )
    >>> bank.deposit( 2,  50 )
    >>> bank.consult( 1 )
    100
    >>> bank.consult( 2 )
    50

    >>> bank.withdraw( 1, 60 )
    >>> bank.withdraw( 2, 100 )
    >>> bank.consult( 1 )
    40
    >>> bank.consult( 2 )
    -50

    >>> bank.transfer( 1, 2, 10 )
    >>> bank.consult( 1 )
    30
    >>> bank.consult( 2 )
    -40


    Multiple operations are supported. They're atomic, just one transaction:

    >>> bank.multipleConsult( [ 1, 2 ] )
    [30, -40]

    >>> bank.deposit( 3, 100 )
    >>> bank.multipleTransfer( [ 1, 3, 3 ], [ 2, 2, 1 ], [ 10, 20, 30 ] )
    >>> bank.multipleConsult( [ 1, 2, 3 ] )
    [50, -10, 50]


    You can also load some data to database (avoid using this):

    >>> bank.__db_update__( { 100: 1000, 200: 2000 } )
    >>> bank.consult( 100 )
    1000


    @see: L{db.DB}
    """
    def __init__( self, db=None ):
        """Create a new Bank.

        @param db: data base to be used. If not provided a new instance will
                   be created
        @type  db: db.DB

        @rtype: L{Bank}
        """
        from db import DB

        if db is None:
            db = DB()

	if not isinstance( db, DB ):
	    raise TypeError( '"db" must be a DB instance' )
	self.__db = db
    # __init__()


    def status( self ):
        return self.__db.status()
    # status()


    def __db_load__( self, file ):
        """Load DB from file.

        @param file: where to load data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.
                     Look at L{db.DB.__db_load__}
        @see: L{db.DB.__db_load__}
        """
	self.__db.__db_load__( file )
    # __db_load__()


    def __db_save__( self, file ):
        """Save DB to file.

        @param file: where to save data.
        @type  file: filename (L{str}) or L{file<__builtin__.file>} object.
                     Look at L{db.DB.__db_save__}
        @see: L{db.DB.__db_save__}
        """
	self.__db.__db_save__( file )
    # __db_save__()


    def withdraw( self, account, value ):
        """Remove C{value} amount from C{account}.

        @param account: Account to operate on

        @param value: How much money to remove.
        @type  value: L{int}
        """
	assert( value > 0 )
	self.__db.begin()
	self.__do_withdraw( account, value )
	self.__db.end()
    # withdraw()


    def deposit( self, account, value ):
        """Add C{value} amount to C{account}.

        @param account: Account to operate on

        @param value: How much money to add.
        @type  value: L{int}
        """
	assert( value > 0 )
	self.__db.begin()
	self.__do_deposit( account, value )
	self.__db.end()
    # deposit()


    def transfer( self, src, dst, value ):
        """Transfers C{value} amount from C{src} to C{dst}.

        @param src: Which account to remove money.
        @param dst: Which account to add money.

        @param value: How much money to transfer.
        @type  value: L{int}
        """
	assert( value > 0 )
	self.__db.begin()
	self.__do_transfer( src, dst, value )
	self.__db.end()
    # transfer()


    def multipleTransfer( self, src_list, dst_list, value_list ):
        """Multiple Transfers.

        Operation is done in order, like this::

          for i in xrange(len(src_list)):
             transfer( src_list[i], dst_list[i], value_list[i] )

        @param src_list: List of source accounts to operate on.
        @type  src_list: L{list} of accounts
        @param dst_list: List of destination accounts to operate on.
        @type  dst_list: L{list} of accounts

        @param value_list: List of how much money to transfer.
        @type  value_list: L{list} of L{int}s

        @see: L{transfer}
        """
	assert( len( src_list ) == len( dst_list ) == len( value_list ) )
	for v in value_list:
	    assert( v > 0 )

	self.__db.begin()
	for src, dst, value in zip( src_list, dst_list, value_list ):
	    self.__do_transfer( src, dst, value )
	self.__db.end()
    # multipleTransfer()


    def consult( self, account ):
        """Consult how much money an account have.

        @param account: Account to operate on.
        @return: how much money the account have.
        @rtype: L{int}
        """
	self.__db.begin()
	v = self.__do_consult( account )
	self.__db.end()
	return v
    # consult()


    def multipleConsult( self, account_list ):
        """Multiple Consult.

        Operation is done in order, like this::

          result = []
          for account in account_list:
             result.append( consult( account ) )

        @param account_list: List of Accounts to operate on.
        @return: list of account money
        @rtype: L{list} of L{int}s

        @see: L{consult}
        """
	self.__db.begin()
	l = [ self.__do_consult( acc ) for acc in account_list ]
	self.__db.end()
	return l
    # multipleConsult()


    def __do_withdraw( self, account, value ):
        """Internal Withdraw operation.

        This may be used within a transaction, since it doesn't starts or
        ends one.

        @param account: Account to operate on.
        @param value: How much money to remove.
        @type  value: L{int}
        """
	old = self.__do_consult( account )
	self.__db.set( account, old - value )
    # __do_withdraw()


    def __do_deposit( self, account, value ):
        """Internal Deposit operation.

        This may be used within a transaction, since it doesn't starts or
        ends one.

        @param account: Account to operate on.
        @param value: How much money to add.
        @type  value: L{int}
        """
	old = self.__do_consult( account )
	self.__db.set( account, old + value )
    # __do_deposit()


    def __do_transfer( self, src, dst, value ):
        """Internal Withdraw operation.

        This may be used within a transaction, since it doesn't starts or
        ends one.

        @param src: Which account to remove money.
        @param dst: Which account to add money.

        @param value: How much money to transfer.
        @type  value: L{int}
        """
	self.__do_withdraw( src, value )
	self.__do_deposit( dst, value )
    # __do_transfer()


    def __do_consult( self, account ):
        """Internal Consult operation.

        This may be used within a transaction, since it doesn't starts or
        ends one.

        Ids that doesn't exist on DB are considered to have 0 money.

        @param account: Account to operate on.
        @return: how much money the account have.
        @rtype: L{int}
        """
	try:
	    return self.__db.get( account )
	except db.DBInvalidId:
	    return 0
    # __do_consult()


    def __db_update__( self, data ):
	"""Update data base contents.

        @param data: data to put in data base.
        @type  data: L{dict}

        @warning: Use with care!

        @see: L{db.DB.__db_update__}
        """
	self.__db.__db_update__( data )
    # __update_db__()


    def __db_clear__( self ):
        """Clear data base contents.

        @warning: Use with care!
        """
        self.__db.__db_clear__()
    # __db_clear__()


    def __str__( self ):
        """Bank representation.

        @rtype: L{str}
        """
        return "%s: \n%s" % ( self.__class__.__name__, self.__db )
    # __str__()
# Bank










class VersionedBank( Bank ):
    """
    Bank API with versioned DataBase.


    Creation:

    >>> vb = VersionedBank()
    >>> vb.__db_clear__() # Force DB being Empty. Since DB is shared using
    ...                   # singletons, it can contain data from previous
    ...                   # tests.


    Version Increases as you update data:

    >>> vb.deposit( 1, 10.0 )
    >>> vb.get_db_version()
    1
    >>> vb.transfer( 1, 2, 5.0 )
    >>> vb.get_db_version()
    2


    You can retrieve what data changed between different versions:

    >>> vb.db_diff( 0 )
    {1: 5.0, 2: 5.0}

    @see: L{db.VersionedDB}
    """
    def __init__( self, db=None ):
        """Create a new Versioned Bank.

        @param db: data base to be used. If not provided a new instance will
                   be created
        @type  db: db.VersionedDB

        @rtype: L{VersionedBank}
        """
        from db import VersionedDB

        if db is None:
            db = VersionedDB()

	if not isinstance( db, VersionedDB ):
	    raise TypeError( '"db" must be a VersionedDB instance' )

        Bank.__init__( self, db=db )
        self.__db = db
    # __init__()


    def shrink_db_journal( self, size ):
        """Shrink database journal to last 'size' entries.

        @note: After this command is called, you cannot get differences from
               forgotten versions.

        @postcondition: database journal will have at most C{size} entries.

        @param size: how many entries to keep.
        @type  size: L{int}
        @see: L{db.VersionedDB.shrink_journal}
        """
        self.__db.shrink_journal( size )
    # shrink_journal()


    def get_db_version( self ):
        """Return database version.

        Database version is incremented on each value update. For example,
        transfer() increases version 2 times.

        @return: data base version.
        @rtype: L{int}
        """
        return self.__db.get_version()
    # get_db_version()


    def db_diff( self, version1, version2=None ):
        """Return changed data between 2 versions.

        This data can be passed to L{db.DB.__db_update__} or
        L{Bank.__db_update__}.

        @param version1: previous version.
        @type  version1: L{int}
        @param version2: next version. If C{None}, latest version is used.
        @type  version2: L{int}

        @return: dictionary with changed data.
        @rtype: L{dict}

        @see: L{db.VersionedDB.diff}
        """
        return self.__db.diff( version1, version2 )
    # db_diff()


    def db_journal_entries( self, version1, version2=None ):
        """Return journal entries between versions.

        @param version1: previous version.
        @type  version1: L{int}
        @param version2: next version. If C{None}, latest version is used.
        @type  version2: L{int}

        @return: list of journal entries as pairs (id, value).
        @rtype: L{list} of L{tuple}s

        @see: L{db.VersionedDB.journal_entries}
        """
        return self.__db.journal_entries( version1, version2 )
    # db_journal_entries()
# VersionedBank
